﻿using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

namespace ServerUtils.Test
{
    [TestClass]
    public class OperationInvokerTest
    {
        private OperationInvoker target;
        private TestOperation operation;
        private TestAsyncOperation asyncOperation;
        private List<LogEventArgs> logs;

        [TestInitialize]
        public void Initialize()
        {
            this.target = new OperationInvoker("invoker-id");
            this.target.Log += this.TargetLog;

            this.logs = new List<LogEventArgs>();

            this.operation = new TestOperation(3);
            this.asyncOperation = new TestAsyncOperation(30);
        }

        private void TargetLog(object sender, LogEventArgs e)
        {
            this.logs.Add(e);
        }

        [TestMethod]
        public void GivenOperationInvoker_OperationInvokeIdIsSet()
        {
            Assert.AreEqual("invoker-id", target.OperationInvokerId);
        }

        [TestMethod]
        public void GivenOperation_OperationIdIsIncreased()
        {
            target.Invoke(this.operation);
            Assert.AreEqual(1, this.operation.OperationId);

            target.Invoke(this.operation);
            Assert.AreEqual(2, this.operation.OperationId);

            var result = target.InvokeAsync<OperationResult>(this.asyncOperation).Result;
            Assert.AreEqual(3, this.asyncOperation.OperationId);
        }

        [TestMethod]
        public void GivenOperation_ExecuteGetsCalled()
        {
            target.Invoke(this.operation);
            Assert.AreEqual(1, this.operation.ExecuteCount);

            target.Invoke(this.operation);
            Assert.AreEqual(2, this.operation.ExecuteCount);

            var result = target.InvokeAsync(this.asyncOperation).Result;
            Assert.AreEqual(1, this.asyncOperation.ExecuteAsyncCount);
        }

        [TestMethod]
        public void GivenOperation_ExecuteResultIsReturned()
        {
            OperationResult result;

            result = target.Invoke(this.operation);
            Assert.AreSame(this.operation.TestResult, result);

            result = target.InvokeAsync(this.asyncOperation).Result;
            Assert.AreSame(this.asyncOperation.TestResult, result);
        }

        [TestMethod]
        public void GivenLogOperationFalse_LogEventIsNotRaised()
        {
            this.target.LogOperation = false;

            this.target.Invoke(this.operation);
            Assert.AreEqual(0, this.logs.Count);

            var result = this.target.InvokeAsync(this.asyncOperation).Result;
            Assert.AreEqual(0, this.logs.Count);
        }

        [TestMethod]
        public void GivenLogOperationTrue_InformationLogEventIsRaised()
        {
            this.target.LogOperation = true;
            this.target.Invoke(this.operation);
            var result = this.target.InvokeAsync(this.asyncOperation).Result;

            string expectedMessage = @"Request:
OperationInvokerId: invoker-id, OperationId: 1, OperationName: test-operation";

            Assert.AreEqual(2, this.logs.Count);
            Assert.AreEqual(EventLogEntryType.Information, this.logs[0].Type);
            StringAssert.StartsWith(this.logs[0].Message, expectedMessage);
        }

        [TestMethod]
        public void GivenFailedOperation_WarningLogEventIsRaised()
        {
            this.operation.TestResult = OperationResult.Failed("operation-failed");
            this.asyncOperation.TestResult = OperationResult.Failed("operation-failed");
            this.target.Invoke(this.operation);
            var result = this.target.InvokeAsync(this.asyncOperation).Result;

            string expectedMessage = @"Unsuccessful operation:
OperationInvokerId: invoker-id, OperationId: 1, OperationName: test-operation";

            Assert.AreEqual(2, this.logs.Count);
            Assert.AreEqual(EventLogEntryType.Warning, this.logs[0].Type);

            StringAssert.StartsWith(this.logs[0].Message, expectedMessage);
        }

        [TestMethod]
        public void GivenExceptionInOperation_ErrorLogEventIsRaised()
        {
            this.operation.Exception = new Exception("test-exception");
            this.asyncOperation.Exception = new Exception("test-exception");
            this.target.Invoke(this.operation);
            var result = this.target.InvokeAsync(this.asyncOperation).Result;

            string expectedMessage = @"Error in operation:
OperationInvokerId: invoker-id, OperationId: 1, OperationName: test-operation";

            Assert.AreEqual(2, this.logs.Count);
            Assert.AreEqual(EventLogEntryType.Error, this.logs[0].Type);

            StringAssert.StartsWith(this.logs[0].Message, expectedMessage);
            StringAssert.Contains(this.logs[0].Message, "test-exception");
        }

        public class TestOperation : IOperation<OperationResult>
        {
            private readonly int request;

            public TestOperation(int request)
            {
                this.request = request;
                this.TestResult = OperationResult.Succeed;
            }

            public int ExecuteCount { get; private set; }
            public long OperationId { get; set; }
            public string Name { get { return "test-operation"; } }
            public object Request { get { return request; } }
            public OperationResult TestResult { get; set; }
            public Exception Exception { get; set; }

            public OperationResult Execute()
            {
                this.ExecuteCount++;

                if (this.Exception != null)
                {
                    throw this.Exception;
                }

                return this.TestResult;
            }
        }

        public class TestAsyncOperation : IAsyncOperation<OperationResult>
        {
            private readonly int request;

            public TestAsyncOperation(int request)
            {
                this.request = request;
                this.TestResult = OperationResult.Succeed;
            }

            public int ExecuteAsyncCount { get; private set; }
            public long OperationId { get; set; }
            public string Name { get { return "test-operation"; } }
            public object Request { get { return request; } }
            public OperationResult TestResult { get; set; }
            public Exception Exception { get; set; }

            public Task<OperationResult> ExecuteAsync()
            {
                return Task.Run(() =>
                {
                    this.ExecuteAsyncCount++;

                    if (this.Exception != null)
                    {
                        throw this.Exception;
                    }

                    return this.TestResult;
                });
            }
        }
    }
}
